/**
 * JDBM LICENSE v1.00
 *
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "JDBM" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Cees de Groot.  For written permission,
 *    please contact cg@cdegroot.com.
 *
 * 4. Products derived from this Software may not be called "JDBM"
 *    nor may "JDBM" appear in their names without prior written
 *    permission of Cees de Groot.
 *
 * 5. Due credit should be given to the JDBM Project
 *    (http://jdbm.sourceforge.net/).
 *
 * THIS SOFTWARE IS PROVIDED BY THE JDBM PROJECT AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * CEES DE GROOT OR ANY CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000 (C) Cees de Groot. All Rights Reserved.
 * Copyright 2000-2001 (C) Alex Boisvert. All Rights Reserved.
 * Contributions are Copyright (C) 2000 by their associated contributors.
 *
 * $Id: CacheRecordManager.java,v 1.9 2005/06/25 23:12:32 doomdark Exp $
 */

package com.sunchangming.stmdb.recman;

import java.io.IOException;
import java.util.Iterator;

import com.sunchangming.stmdb.RecordManager;
import com.sunchangming.stmdb.helper.CacheEvictionException;
import com.sunchangming.stmdb.helper.CachePolicy;
import com.sunchangming.stmdb.helper.CachePolicyListener;
import com.sunchangming.stmdb.helper.Serializer;
import com.sunchangming.stmdb.helper.WrappedRuntimeException;

/**
 * A RecordManager wrapping and caching another RecordManager.
 * 
 * @author <a href="mailto:boisvert@intalio.com">Alex Boisvert</a>
 * @author <a href="cg@cdegroot.com">Cees de Groot</a>
 * @version $Id: CacheRecordManager.java,v 1.9 2005/06/25 23:12:32 doomdark Exp
 *          $
 */
public class CacheRecordManager implements RecordManager {

	/**
	 * Wrapped RecordManager
	 */
	protected RecordManager _recman;

	/**
	 * Cache for underlying RecordManager
	 */
	protected CachePolicy _cache;

	/**
	 * Construct a CacheRecordManager wrapping another RecordManager and using a
	 * given cache policy.
	 * 
	 * @param recman
	 *            Wrapped RecordManager
	 * @param cache
	 *            Cache policy
	 */
	public CacheRecordManager(RecordManager recman, CachePolicy cache) {
		if (recman == null) {
			throw new IllegalArgumentException("Argument 'recman' is null");
		}
		if (cache == null) {
			throw new IllegalArgumentException("Argument 'cache' is null");
		}
		_recman = recman;
		_cache = cache;

		_cache.addListener(new CacheListener());
	}

	/**
	 * Get the underlying Record Manager.
	 * 
	 * @return underlying RecordManager or null if CacheRecordManager has been
	 *         closed.
	 */
	public RecordManager getRecordManager() {
		return _recman;
	}

	/**
	 * Get the underlying cache policy
	 * 
	 * @return underlying CachePolicy or null if CacheRecordManager has been
	 *         closed.
	 */
	public CachePolicy getCachePolicy() {
		return _cache;
	}

	/**
	 * Inserts a new record using a custom serializer.
	 * 
	 * @param obj
	 *            the object for the new record.
	 * @param serializer
	 *            a custom serializer
	 * @return the rowid for the new record.
	 * @throws IOException
	 *             when one of the underlying I/O operations fails.
	 */
	@Override
	public synchronized long insert(Object obj, Serializer serializer) throws IOException {
		checkIfClosed();

		final long recid = _recman.insert(obj, serializer);
		try {
			_cache.put(Long.valueOf(recid), new RecCacheEntry(recid, obj, serializer, false));
		} catch (final CacheEvictionException except) {
			throw new WrappedRuntimeException(except);
		}
		return recid;
	}

	/**
	 * Deletes a record.
	 * 
	 * @param recid
	 *            the rowid for the record that should be deleted.
	 * @throws IOException
	 *             when one of the underlying I/O operations fails.
	 */
	@Override
	public synchronized void delete(long recid) throws IOException {
		checkIfClosed();

		_recman.delete(recid);
		_cache.remove(Long.valueOf(recid));
	}

	/**
	 * Updates a record using a custom serializer.
	 * 
	 * @param recid
	 *            the recid for the record that is to be updated.
	 * @param obj
	 *            the new object for the record.
	 * @param serializer
	 *            a custom serializer
	 * @throws IOException
	 *             when one of the underlying I/O operations fails.
	 */
	@Override
	public synchronized void update(long recid, Object obj, Serializer serializer) throws IOException {
		RecCacheEntry entry;
		Long id;

		checkIfClosed();

		id = Long.valueOf(recid);
		try {
			entry = _cache.get(id);
			if (entry != null) {
				// reuse existing cache entry
				entry._obj = obj;
				entry._serializer = serializer;
				entry._isDirty = true;
			} else {
				_cache.put(id, new RecCacheEntry(recid, obj, serializer, true));
			}
		} catch (final CacheEvictionException except) {
			throw new IOException(except.getMessage());
		}
	}

	/**
	 * Fetches a record using a custom serializer.
	 * 
	 * @param recid
	 *            the recid for the record that must be fetched.
	 * @param serializer
	 *            a custom serializer
	 * @return the object contained in the record.
	 * @throws IOException
	 *             when one of the underlying I/O operations fails.
	 */
	@Override
	public synchronized Object fetch(long recid, Serializer serializer) throws IOException {
		checkIfClosed();

		final Long id = Long.valueOf(recid);
		RecCacheEntry entry = _cache.get(id);
		if (entry == null) {
			entry = new RecCacheEntry(recid, null, serializer, false);
			entry._obj = _recman.fetch(recid, serializer);
			try {
				_cache.put(id, entry);
			} catch (final CacheEvictionException except) {
				throw new WrappedRuntimeException(except);
			}
		}
		return entry._obj;
	}

	/**
	 * Closes the record manager.
	 * 
	 * @throws IOException
	 *             when one of the underlying I/O operations fails.
	 */
	@Override
	public synchronized void close() throws IOException {
		checkIfClosed();

		updateCacheEntries();
		_recman.close();
		_recman = null;
		_cache = null;
	}

	/**
	 * Returns the number of slots available for "root" rowids. These slots can
	 * be used to store special rowids, like rowids that point to other rowids.
	 * Root rowids are useful for bootstrapping access to a set of data.
	 */
	@Override
	public synchronized int getRootCount() {
		checkIfClosed();

		return _recman.getRootCount();
	}

	/**
	 * Returns the indicated root rowid.
	 * 
	 * @see #getRootCount
	 */
	@Override
	public synchronized long getRoot(int id) throws IOException {
		checkIfClosed();

		return _recman.getRoot(id);
	}

	/**
	 * Sets the indicated root rowid.
	 * 
	 * @see #getRootCount
	 */
	@Override
	public synchronized void setRoot(int id, long rowid) throws IOException {
		checkIfClosed();

		_recman.setRoot(id, rowid);
	}

	/**
	 * Commit (make persistent) all changes since beginning of transaction.
	 */
	@Override
	public synchronized void commit() throws IOException {
		checkIfClosed();
		updateCacheEntries();
		_recman.commit();
	}

	/**
	 * Rollback (cancel) all changes since beginning of transaction.
	 */
	@Override
	public synchronized void rollback() throws IOException {
		checkIfClosed();

		_recman.rollback();

		// discard all cache entries since we don't know which entries
		// where part of the transaction
		_cache.removeAll();
	}

	/**
	 * Obtain the record id of a named object. Returns 0 if named object doesn't
	 * exist.
	 */
	@Override
	public synchronized long getNamedObject(String name) throws IOException {
		checkIfClosed();

		return _recman.getNamedObject(name);
	}

	/**
	 * Set the record id of a named object.
	 */
	@Override
	public synchronized void setNamedObject(String name, long recid) throws IOException {
		checkIfClosed();

		_recman.setNamedObject(name, recid);
	}

	/**
	 * Check if RecordManager has been closed. If so, throw an
	 * IllegalStateException
	 */
	private void checkIfClosed() throws IllegalStateException {
		if (_recman == null) {
			throw new IllegalStateException("RecordManager has been closed");
		}
	}

	/**
	 * Update all dirty cache objects to the underlying RecordManager.
	 */
	protected void updateCacheEntries() throws IOException {
		final Iterator<com.sunchangming.stmdb.helper.CacheEntry> enume = _cache.elements();
		while (enume.hasNext()) {
			final RecCacheEntry entry = enume.next().getValue();
			if (entry._isDirty) {
				_recman.update(entry._recid, entry._obj, entry._serializer);
				entry._isDirty = false;
			}
		}
	}

	private class CacheListener implements CachePolicyListener {

		/**
		 * Notification that cache is evicting an object
		 * 
		 * @arg obj object evited from cache
		 * 
		 */
		@Override
		public void cacheObjectEvicted(Object obj) throws CacheEvictionException {
			final RecCacheEntry entry = (RecCacheEntry) obj;
			if (entry._isDirty) {
				try {
					_recman.update(entry._recid, entry._obj, entry._serializer);
				} catch (final IOException except) {
					throw new CacheEvictionException(except);
				}
			}
		}

	}
}
